結論先說:框架選對只是起點,真正驗證它有沒有用,要靠實作把三套框架接上真實資料流,再用受控事故情境確認每套框架真的能導向不同判斷;同時也要清楚它們回答不了什麼問題。
承接上文:上篇(上)談完 Golden Signals、RED、USE 三套框架各自的定位與差異、AI 時代多出來的 workflow/資源訊號,並示範怎麼把框架落成一份用 Pydantic 驗證、可版控 review 的指標契約(outcome 有限集合、label 基數控制)。本篇(下)接著做 SRE Lab 實作、事故情境驗證、框架的反例與限制,最後回到 dashboard 分層與今日所學。
/ask 同時產生 RED、USE 與 AI 訊號這節提供讀者可自行完成的實作路徑。本文沒有在此 repository 建立、安裝、啟動或驗證 Day 22 的 DIY;請在自己的 Day 22/DIY 獨立專案中操作,並把版本與輸出記在 README。以下程式刻意只示範觀測邊界,不替你連接任何真實模型或 GPU。
這一節要驗證三件前面只用文字描述的事:histogram 比平均值更能反映真實延遲分佈(呼應②、④節);label 的基數必須事先限制在有限集合(呼應⑤節的 outcome 契約);量測點要放在 workflow 的正確邊界,而非隨手在函式裡塞 timer。
你需要:
POST /ask 的 FastAPI 範例,或能以 stub 模擬 workflow。/metrics。request_id 與 Day 23 將使用的 trace context 規劃。若沒有真實 GPU,仍可保留 queue_wait 與 queue_depth 指標,用 in-memory queue 模擬——不要把模擬數字誤當成 GPU capacity benchmark。這節刻意不要求真的接上 LLM 或 GPU:目標是驗證「觀測資料流有沒有正確連通」——從 observe() 呼叫、/metrics 能否看到對應 bucket、Prometheus 能否 scrape 到,到 PromQL 能否算出合理數字,任一段斷掉,dashboard 都會空白或數字不合理,問題常常不在 Grafana,在更前面。
在你的 app/metrics.py 中建立 metrics。Prometheus histogram 會暴露 bucket、count、sum;因此才能在後端計算跨 instance 的 percentile。官方文件也提醒,不要平均各 instance 的 summary quantile;聚合 histogram bucket 才是可用的做法。Prometheus histogram practices
from prometheus_client import Counter, Gauge, Histogram
REQUESTS = Counter(
"ai_requests_total",
"Completed AI requests by externally visible outcome.",
("route", "outcome", "model_route"),
)
REQUEST_DURATION = Histogram(
"ai_request_duration_seconds",
"End-to-end duration for an AI request.",
("route", "outcome"),
buckets=(0.1, 0.3, 0.5, 1, 2, 5, 10, 30),
)
TTFT = Histogram(
"ai_ttft_seconds",
"Time from accepted request to the first streamed token.",
("route", "model_route"),
buckets=(0.1, 0.25, 0.5, 1, 2, 5, 10),
)
QUEUE_WAIT = Histogram(
"ai_queue_wait_seconds",
"Time waiting for an inference worker.",
("queue", "model_route"),
buckets=(0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10),
)
QUEUE_DEPTH = Gauge(
"ai_inference_queue_depth",
"Requests waiting at the instant of observation.",
("queue",),
)
WORKFLOW_STEPS = Counter(
"ai_workflow_steps_total",
"AI workflow step outcomes.",
("step", "outcome"),
)
這裡選 histogram 而非只存一個平均值,因為平均值會把少數非常慢的使用者壓平。若有 99 個請求在 300 ms 內完成、1 個請求等 30 秒,平均約 597 ms 看起來「還好」;那一位等 30 秒的使用者不會同意。
REQUEST_DURATION 的 bucket 邊界是 (0.1, 0.3, 0.5, 1, 2, 5, 10, 30),不是隨手取的等距數字:bucket 越密集,quantile 越精確,但每個 bucket 都是額外時間序列,會墊高儲存與查詢成本。取捨原則是「SLO 目標附近放密一點」——/ask 的 P95 目標若是 2 秒內,0.1~2 這幾格能精確看出離目標還有多少餘裕;5、10、30 秒這幾格只需標記「已經明顯壞掉」。越靠近決策邊界,解析度越高,這條原則不只適用於 bucket。
QUEUE_WAIT 的 bucket 起點特別低(0.01 秒):worker 有空時 queue wait 該接近零,起點設太高會讓「沒排隊」和「排了 50 毫秒」落進同一格,看不出排隊剛形成的早期訊號。
以下以同步 stub 表示結構。你的系統可能用 async、streaming response 或背景 queue;量測點仍應保留:接受請求、進入 worker、第一個 token、完成 workflow、取得 outcome。
from time import monotonic
from fastapi import FastAPI, HTTPException
from app.metrics import (
QUEUE_DEPTH,
QUEUE_WAIT,
REQUEST_DURATION,
REQUESTS,
TTFT,
WORKFLOW_STEPS,
)
app = FastAPI()
def answer_from_workflow(question: str) -> dict[str, str]:
# 以你自己的 retrieval、LLM、tool、validator 實作取代。
# 不要在 Prometheus label 放入 question 或原始答案。
return {"answer": f"stub answer for: {question}", "outcome": "ok"}
@app.post("/ask")
def ask(question: str) -> dict[str, str]:
route = "/ask"
model_route = "primary"
request_started = monotonic()
queue_name = "inference"
outcome = "technical_error"
QUEUE_DEPTH.labels(queue=queue_name).inc()
queued_at = monotonic()
try:
# 真實系統在此等待 semaphore 或 worker queue;stub 直接取得 worker。
worker_acquired_at = monotonic()
QUEUE_WAIT.labels(
queue=queue_name,
model_route=model_route,
).observe(worker_acquired_at - queued_at)
retrieval_started = monotonic()
result = answer_from_workflow(question)
WORKFLOW_STEPS.labels(step="workflow", outcome=result["outcome"]).inc()
# 串流服務應在真正送出第一個 token 時 observe,不能用 workflow 完成代替。
TTFT.labels(route=route, model_route=model_route).observe(
monotonic() - retrieval_started
)
outcome = result["outcome"]
REQUESTS.labels(
route=route,
outcome=outcome,
model_route=model_route,
).inc()
return result
except TimeoutError as exc:
outcome = "upstream_timeout"
REQUESTS.labels(
route=route,
outcome=outcome,
model_route=model_route,
).inc()
raise HTTPException(status_code=504, detail="upstream timeout") from exc
finally:
QUEUE_DEPTH.labels(queue=queue_name).dec()
REQUEST_DURATION.labels(route=route, outcome=outcome).observe(
monotonic() - request_started
)
範例在 try 前先把 outcome 初始化為 technical_error:workflow 發生未預期例外時,finally 仍能記錄完整 duration,不會因未賦值變數掩蓋原始錯誤。正式程式應只把已知例外映射成更細的 outcome,並為這個 mapping 寫測試。
對 QUEUE_DEPTH 也要同樣誠實:上例只能表達這個 process 內的計數,多 worker 或外部 queue 不能靠各 process 的 Gauge 相加宣稱是全域 queue depth,應由 queue 系統的 exporter 輸出權威數字。
量測點放錯位置是實務上最常見、卻最不容易在 code review 時被抓出來的錯誤:
QUEUE_DEPTH.inc() 放在 try 之外——只要請求被接受就該算進計數;挪進 try 內部、放在可能提早 return 的邏輯之後,某些路徑會漏算,導致只增不減、長期偏移(第 ⑧ 段再深入談這個 Gauge 陷阱)。QUEUE_WAIT 只量「取得 worker」這段區間,不含輸入驗證與認證檢查——這才是 USE 的 Saturation 想回答的問題:這個資源造成了多少排隊。TTFT 的量測點刻意寫了警告註解:stub 用「retrieval 完成到 workflow 結束」近似 TTFT,但真實串流系統裡該是「請求進來」到「第一個 byte 寫進 response stream」——這是「指標存在不等於量對東西」的具體案例。REQUEST_DURATION 放在 finally——確保無論 workflow 走哪條路徑結束,耗時都會被完整記錄,不會因某條錯誤處理路徑忘記 observe,讓那類失敗從 duration 分佈裡消失。在自己的 prometheus.yml 加入對應 target;port、路徑與 service name 依你的 compose 或本機執行方式調整。
scrape_configs:
- job_name: ai-api
metrics_path: /metrics
static_configs:
- targets:
- ai-api:8000
啟動方式依你 Day 22/DIY README 記錄的環境操作。驗證時先開 /metrics 確認能看到 ai_requests_total、histogram 的 _bucket/_count/_sum,再把 query 貼進 Prometheus——先確認資料存在,再懷疑 Grafana。
Day22/DIY 這個獨立專案的 Makefile 把驗證流程拆成兩個獨立指令:make generate 和 make server + make test-endpoint。對應本文一路強調的原則:設計對不對,跟程式碼跑起來對不對,是兩個不同層次的問題,不該混在一起驗證。
make generate → 驗證「設計」
跑 Pydantic 模型,檢查三層是否都存在、
有沒有套錯方法、AI 特有訊號是否涵蓋齊全
不必啟動伺服器,幾秒內知道規劃合不合理
make server + make test-endpoint → 驗證「執行」
真的啟動 FastAPI、發送真實請求,確認
/ask 能回應、/metrics 能 export 出正確資料、
量測點在真實路徑下真的會被觸發
驗證的是「規劃寫得再好,程式碼有沒有真的照著做」
這呼應第 ④ 節的分層原則:先確定該看什麼問題(設計層),再確定這個問題有沒有被正確量測(執行層)。很多團隊只做後半段——一頭栽進怎麼裝 client、怎麼寫 PromQL,做出來的 dashboard 即使每個數字都精確,仍可能像第①節的十幾張圖一樣,回答了太多沒排優先順序的問題。
「圖是空的」是可觀測性堆疊裡最常見也最挫折的症狀——不會給出任何錯誤訊息,只是安靜地什麼都不顯示。排查順序該從資料源頭往消費端確認:
症狀:Grafana 面板顯示 "No data"
▼
1. curl http://<service>:8000/metrics 看得到 ai_requests_total 嗎?
├─ 看不到 → 問題在應用程式:metric 沒註冊,或這個 code path 從沒被執行過
└─ 看得到 → 繼續往下查
▼
2. Prometheus UI 的 Targets 頁面:ai-api 這個 job 是 UP 嗎?
├─ DOWN → 問題在 scrape 設定:port 不對、network 沒連通、健康檢查失敗
└─ UP → 繼續往下查
▼
3. Prometheus expression browser 直接查 ai_requests_total(不透過 Grafana)
├─ 看不到 → 問題在 relabel 設定或 job 名稱不符
└─ 看得到 → 問題幾乎確定在 Grafana:datasource 選錯、query 語法錯、時間範圍不對
最常見的第一步問題是「這個 code path 根本沒被觸發過」——想看 outcome=upstream_timeout 的曲線,但測試環境從沒觸發過 timeout,Prometheus 裡自然沒有這組 label,Grafana 畫不出東西,這不是故障,只是事件還沒發生。這也是第 ⑦ 節要設計情境 A、B 兩個受控實驗的理由:確保每種你關心的 outcome 都至少被真實觸發過一次。
下列 expression 假設 ai_requests_total 是 Counter。rate() 的視窗要依 scrape interval 和告警目的調整,五分鐘只是教學起點。
# Rate:過去五分鐘,每秒完成多少 /ask 請求
sum(rate(ai_requests_total{route="/ask"}[5m]))
# Errors:技術錯誤與 upstream timeout 的比例
sum(rate(ai_requests_total{
route="/ask",
outcome=~"technical_error|upstream_timeout|tool_failed"
}[5m]))
/
sum(rate(ai_requests_total{route="/ask"}[5m]))
# Duration:跨 instance 聚合後的 P95 end-to-end latency
histogram_quantile(
0.95,
sum by (le) (
rate(ai_request_duration_seconds_bucket{route="/ask"}[5m])
)
)
Prometheus 官方文件以 histogram_quantile() 搭配 rate() 計算 quantile,避免把不同 instance 的 summary quantile 拿來平均的統計錯誤。Prometheus query functions
分母為零時,error ratio 會得到空值或無意義結果。低流量服務不要把單一錯誤直接升級成「5% error」的大事故;同時顯示 request count,並在告警條件中加最小流量門檻。
這三個 panel 的順序對應值班時該依序確認的三個問題:現在還有流量嗎、這些流量裡有多少失敗、成功的花了多久。rate 掉到零時,問題多半在上游而非 /ask 本身,此時看 error ratio 和 duration 沒有意義(分母接近零);rate 正常但 error ratio 飆高,才值得往下查是哪種 outcome 在增加——先確認分母穩定再看分子。
histogram_quantile() 是在 bucket 邊界間做線性插值,不是精確百分位數——bucket 越粗,誤差越大。延遲分佈若明顯雙峰(快取命中極快、未命中慢很多),插值出的「P95」可能落在幾乎沒有樣本的區間,統計上存在卻不對應任何真實請求,這也是為什麼 bucket 要在 SLO 目標附近放密一點。
/ask 的 total duration 很重要,但串流體驗常由 TTFT 決定:使用者看到第一個 token 就會覺得服務在工作;反之 20 秒空白畫面非常像服務壞了。
這有使用者心理研究支撐:低於 100 毫秒感覺瞬間發生,1 秒以上注意力開始離開畫面,超過 10 秒多數人傾向放棄或重整。Why Faster First Tokens Matter More Than Total Response Time 由此帶出一個違反直覺的結論:總耗時 4 秒、TTFT 只有 200 毫秒的系統,體驗會優於總耗時 2 秒、TTFT 卻高達 1.5 秒的系統。TTFT Is the Only Latency Metric Your Users Actually Feel
# P95 TTFT,依 model route 比較 primary 與 fallback
histogram_quantile(
0.95,
sum by (le, model_route) (
rate(ai_ttft_seconds_bucket{route="/ask"}[5m])
)
)
quality 與 safety 不該被偷塞進技術 error rate,需要各自的分子、分母與取樣範圍。若 evaluator 只抽樣 10% 請求,不能把 quality_failed / evaluations_total 寫成全體失敗率——它是被評估樣本中的失敗比例。
# 評估樣本中的 quality failure ratio,不是全部 request 的品質率
sum(rate(ai_evaluations_total{result="quality_failed"}[1h]))
/
sum(rate(ai_evaluations_total[1h]))
把串流回應的延遲體驗拆到最細,其實不只 TTFT 和 total duration 兩個維度,完整來說有三層:
串流回應的三層延遲維度
┌────────────────────────────────────────────────┐
│ TTFT 使用者等第一個字出現多久 │
│ inter-token latency 字與字之間的流暢度(是否卡頓) │
│ end-to-end duration 完整答案輸出完畢的總耗時 │
└────────────────────────────────────────────────┘
本文的 metric contract 只涵蓋 TTFT 和 end-to-end duration,不納入 inter-token latency——刻意取捨,不是遺漏:它需要每次 chunk 輸出都記錄時間戳,寫入頻率高出一個量級,在 Lab 規模的實作裡容易讓寫入負擔變複雜。若服務有「生成過程卡頓」回報而前兩者都正常,它就是下一個該補上的指標。
對 inference queue 而言,最容易讀錯的是把 queue depth 當成唯一飽和度。depth 是某個瞬間的庫存;queue wait 才更接近「使用者為此等了多久」,兩者要成對看。
# 當下等待中的請求數。多 instance 時要先確認 exporter 的聚合語意。
sum(ai_inference_queue_depth{queue="inference"})
# P95 queue wait:使用者在 worker 前等待多久
histogram_quantile(
0.95,
sum by (le) (
rate(ai_queue_wait_seconds_bucket{queue="inference"}[5m])
)
)
GPU utilization、VRAM 使用率與 OOM 事件可從你選擇的 GPU exporter 接入,但 metric name、label 依 exporter 而不同。先讀你實際 exporter 的 /metrics 和文件,再在 USE panel 定義:
Utilization GPU compute 是否忙碌
Saturation queue wait、queue depth、VRAM allocation pressure
Errors OOM、driver/Xid、worker restart、inference failure
如果某張 GPU 圖只會讓人說「嗯,現在是 83%」卻回答不了下一步,先別放首頁——留在 drill-down 頁,或補上 queue wait 與 worker restart 的對照。
「depth 和 wait 要成對看」不是憑空而來。Google 的叢集調度系統 Borg 就圍繞這個洞察設計:關鍵指標之一是 scheduling delay——thread 等待超過 1 毫秒才拿到 CPU 的頻率。Large-scale cluster management at Google with Borg 即使叢集利用率長期在 80% 以上,延遲敏感任務的 scheduling delay 超過 5 毫秒仍只佔極少數時間——關鍵在於嚴格的優先級隔離與 admission control:高優先級任務預留專屬資源,負載逼近上限時主動拒絕新的低優先級任務。
放回 GPU inference queue:只看 ai_inference_queue_depth 的絕對值回答不了「排隊嚴不嚴重」——depth=10 對高併發系統可能正常,對併發度只有 2 的系統卻是災難。真正有意義的是這 10 個請求各自等了多久,也就是 ai_queue_wait_seconds 在量的東西。若系統像本文 stub 範例一樣純 FIFO、沒有優先級與拒絕機制,高負載時所有請求會被同等拖慢。
USE 把 utilization 和 saturation 分成兩個獨立問題不是沒理由的。2023 年 11 月 8 日,OpenAI 的 ChatGPT 與 API 發生約 1.5 小時的大規模中斷,大量請求收到 502/503。官方事故報告 根因是 routing 層的記憶體管理 bug:迴圈裡不斷配置新 buffer,高流量下記憶體持續墊高,最終讓節點連 readiness check 都無法通過,能接單的節點越來越少,形成連鎖式容量塌縮。
放進 USE 框架看,這是「utilization 騙不了人,但也幫不了忙」的案例:CPU utilization 未必顯著升高,真正先出問題的是 saturation——而 readiness check 失敗就是 USE 的 Errors 訊號。若 USE panel 只有一條 utilization 曲線,沒把 saturation 與 errors 攤開,這類故障會一路悶燒到節點所剩無幾才被發現。
OpenAI 的案例是「utilization 正常、saturation 已爆表」;Slack 在 2021 年 1 月 4 日遇到的中斷,展示同一問題的另一種變形:utilization 這個數字本身直接失真,指向錯誤方向。
新年假期結束後第一個工作日,大量使用者幾乎同時重新打開 Slack,流量衝到接近歷史新高,同時 AWS Transit Gateway 大量丟包,請求卡在等待網路 I/O 而非真正運算。Slack's Outage on January 4th 2021 Slack 的 web tier 靠 CPU 使用率驅動 autoscaling,但丟包讓 CPU 使用率反而下降——autoscaler 誤判成「流量變少」,主動把 web tier 縮編。Slack fingers AWS auto-scaling failure 容量被自己的自動化砍掉,疊加網路問題讓中斷惡化;監控告警系統本身也架在同一個受影響的 VPC,連團隊自己診斷的能力都被拖累。
這比 OpenAI 更尖銳:OpenAI 是「utilization 沒說謊,只是答不了 saturation 的問題」;Slack 是「utilization 本身會反過來說謊」。這種代理指標一旦接上自動化決策(如 autoscaler),一次誤讀就會觸發一次真實、立即的錯誤系統行為。
實作完成後,不要只確認 graph 有線。用受控情境確認每個框架都能導向不同判斷。以下測試不要求你模擬真實模型;stub、sleep、可控例外都足以驗證觀測的資料流。
這兩個情境對應第 ④ 節決策表裡「正常卻仍可能有問題」那一欄——情境 A 驗證「技術失敗和資源飽和是兩件不同的事」,情境 B 驗證「HTTP 成功率綠色不代表體驗健康」。同一種「使用者受影響」的結果,背後可能是完全不同的根因鏈,需要完全不同的下一步動作。
讓 model client 在固定時間後丟出 TimeoutError,並在程式中映射為 upstream_timeout。你預期看到:
Golden Signals 使用者看到失敗或降級回覆;error budget 受影響
RED /ask 的 upstream_timeout 比例上升;duration 可能也增加
USE inference queue depth 沒有持續上升;GPU 未必很忙
Trace model span 有 timeout;retrieval 和 queue wait 正常
這個情境防止一個常見誤判:只因為 AI request 失敗,就直接擴 GPU。若問題在外部 provider 或 tool,擴容只會讓更多請求更快撞上同一個依賴。
要實際重現這個情境,最小的改法是在 answer_from_workflow 加一個可控的延遲與逾時開關,用環境變數觸發,避免這段測試邏輯混進正式的商業邏輯:
import os
from time import sleep
CHAOS_UPSTREAM_TIMEOUT = os.getenv("CHAOS_UPSTREAM_TIMEOUT") == "1"
def answer_from_workflow(question: str) -> dict[str, str]:
if CHAOS_UPSTREAM_TIMEOUT:
# 模擬上游 model provider 逾時:不真的等待,
# 直接模擬「等了很久之後失敗」這個結果。
sleep(0.05) # 象徵性延遲,不要真的 sleep 到真實 timeout 秒數
raise TimeoutError("simulated upstream timeout")
return {"answer": f"stub answer for: {question}", "outcome": "ok"}
開啟 CHAOS_UPSTREAM_TIMEOUT=1 後連續送出十幾個請求,預期 ai_requests_total{outcome="upstream_timeout"} 持續增加,而 ai_inference_queue_depth 維持接近零——請求是在取得 worker「之後」才因上游逾時失敗,不涉及排隊。若 queue depth 反而跟著飆升,代表「等待上游」和「等待 worker」被混在同一段程式碼裡量測,值得回頭檢查量測點邊界。
用 semaphore 或 stub queue 把可同時處理的工作限制到一個,接著平行送出多個請求。你預期看到:
Golden Signals TTFT 與 end-to-end P95 上升;使用者開始等待
RED rate 有流量;5xx 仍接近零;duration 尾端變差
USE queue depth、P95 queue wait 上升
Trace 每條 trace 的 queue wait span 變長;model span 未必變慢
這個情境能驗證「成功率綠色」不代表體驗健康。請記錄送出時間、併發數、觀察視窗與實際輸出;不要用目測說 dashboard 看起來有變化。
這個情境比情境 A 更容易被低估——所有請求最終都會成功,沒有任何 error 曲線會動。重現方式:用容量為 1 的 semaphore 包住 worker 取得邏輯:
import asyncio
_worker_semaphore = asyncio.Semaphore(1) # 模擬只有一個 worker 可用
async def acquire_worker() -> None:
await _worker_semaphore.acquire()
def release_worker() -> None:
_worker_semaphore.release()
接著用簡單腳本平行送出 5 到 10 個請求(例如 httpx.AsyncClient 搭配 asyncio.gather),記錄每個請求的送出與回應時間戳。預期現象:第一個請求幾乎立即被服務,後續請求的 queue_wait 依序疊加——第二個要等第一個處理完才能拿到 worker,以此類推。這種線性疊加正是 USE 的 Saturation 想捕捉的現象:resource 本身沒壞,只是工作量超過服務能力。
回頭比對 histogram_quantile(0.95, ...) 算出的 P95 queue wait,應能看到清楚的階梯狀成長——「排隊」造成的延遲跟「單一請求處理變慢」在 trace 上呈現不同樣貌:前者是每個 span 前面多出一段等待,後者是 span 本身變長。
ai_requests_total 的 outcome 是有限集合,沒有 request ID 或 prompt label。/metrics 中能看到 Counter 與 Histogram 的 bucket、count、sum。P95 的意思是約有 5% 請求比它更慢。對低流量但高價值的任務,P99、max、單一關鍵租戶的受影響事件可能更重要;流量很低時 P99 又很不穩定,這時應回到原始 request count、trace 和 log,而非把很少的樣本畫成精準趨勢。
percentile 還有一個常被忽略的特性:無法組合。服務 A 的 P95 是 200 毫秒、服務 B 是 150 毫秒,不能推論「A 呼叫 B 的整條鏈路 P95 是 350 毫秒」——percentile 不像平均值能線性相加,所以多步驟 workflow 必須真正量測整條鏈路,不能拿子步驟的 P95 拼湊。
尾端延遲不一定來自故障,也可能來自系統性但非故障的因素。無伺服器架構的冷啟動是經典案例:函式在首次調用或長時間閒置後重啟,可能額外增加 500 毫秒到數秒延遲。How to Optimize Lambda Cold Starts 對照 AI workflow,model worker 的冷啟動(載入模型權重到 GPU 記憶體)是放大版:一次可能耗時數十秒,若擴縮容頻繁增減 worker,P99 會長期被冷啟動主宰,而 P50 完全看不出異常。
fallback answer、空結果、引用不存在的文件、被 parser 修正後仍不符合任務,可能都以 HTTP 200 結束。Day 07 的 technical success 與 semantic success 在這裡仍然有效:RED error rate 管技術失敗,evaluation 補上語意層,不要混成一個漂亮但無法解釋的百分比。
資源可能受 memory fragmentation、connection pool、mutex 或單執行緒 scheduler 限制。USE 的 saturation 正是為了補上 utilization 看不到的等待;但也別把任何 queue 都稱作 saturation,先確認它確實代表被同一資源阻擋的工作。
timer 放錯位置、retry 被重複計數、outcome 映射不一致、worker crash 後 Gauge 未歸零,都會讓 dashboard 產生合理但錯誤的圖。寫 unit test 驗證 outcome mapping,並在 integration test 送入已知 timeout、已知 validator failure,會比盯著曲線猜更可靠。這四種錯誤症狀相似(平滑、合理、沒有明顯異常),修法卻完全不同:
ai_requests_total,會把「一次體驗」膨脹成「三次請求」並稀釋成功率。正確做法是只在使用者可見邊界 observe 一次,內部重試用獨立 counter 追蹤。finally)沒機會 .dec(),ai_inference_queue_depth 會永久偏高——多 worker 系統通常需要由 queue 本身的 exporter 輸出權威數字。本文的五分鐘視窗、P95 查詢與 bucket 都是教學範例,實際 SLO 要由使用者旅程、容量、成本與錯誤預算共同決定。把別人 dashboard 的 > 2 seconds 直接複製過來,只是把一個看起來精準的數字換成未經承諾的告警噪音。
不只是別人的閾值不能照搬,自己過去訂的也不是一勞永逸:服務剛上線時 P95 訂 2 秒可能合理,半年後流量成長十倍、workflow 多了新步驟——還沿用舊門檻,要嘛長期告警疲勞,要嘛早該收緊卻沒人注意。這也是第 ⑩ 節 panel 規格加上 last_reviewed 欄位的原因。
傳統架構的尾端延遲多能被 timeout 機制吸收,但 AI 系統暴露了更深的挑戰:研究者觀察到 LLM Agent 系統會進入一種難以察覺的狀態——技術指標全部綠燈,行為卻已悄悄偏離正常範圍,只有事後系統性量測才驗得出來,這被稱為 silent failure。Silent Failure in LLM Agent Systems 典型案例是 RAG 系統:RED 監控 API、USE 監控 GPU 都抓不到「檢索器回傳錯誤文件、LLM 基於不完整上下文編造答案」這類故障,往往要等到客戶投訴才發現輸出品質已不可接受。
這不只是學術推論。2025 年 8 到 9 月,Anthropic 公開承認撞上同一件事:三個獨立的基礎設施 bug(routing 誤導向、TPU 輸出亂碼、編譯器誤算 top-k)讓 Claude 部分回應悄悄壞掉,但 API 層級完全正常——沒有 5xx、沒有 timeout、stop_reason 照樣正常結束。Anthropic 的事後報告 他們的驗證流程仰賴 benchmark、安全評測與效能指標,就是沒有抓到使用者回報的品質下降。三個框架全部綠燈,真正發現問題的是使用者回報。這說明 RED、USE 仍必要,但 AI 系統要再加第三層 Quality Signals(evaluation outcome、hallucination detection、retrieval quality),下一篇 Day 23 會探討。
把 Anthropic 這次事故逐一對照今天談的三套框架,能更精確地定位 Quality Signals 這一層到底補的是什麼缺口:
| 框架 | 理論上該回答的問題 | 這起事故中的實際表現 |
|---|---|---|
| Golden Signals | 使用者現在好不好 | latency、traffic、error rate 全部正常;「使用者好不好」被簡化成可用性代理指標,而代理指標剛好失真 |
| RED | /ask 是否變慢或失敗 |
request rate 正常、5xx 沒增加、duration 沒異常上升——三個維度全部綠燈 |
| USE | 資源是否撐不住 | TPU 設定錯誤不會反映在 utilization 或 saturation 上;它是運算「結果」錯了,不是資源「不夠」 |
| Quality Signals(缺失的第四層) | 輸出是否正確 | 唯一能發現問題的一層,但當時系統裡沒把它做成跟前三層一樣即時、一樣受重視的訊號 |
這張表想凸顯:不是三套框架做得不夠好,而是它們誕生的年代根本不存在「輸出是否正確」這種問題形態。要求 RED 抓到「答案內容錯了但技術上跑完了」,就像要求體溫計量出血壓——不是體溫計壞了,是量錯了維度,這也是 evaluation outcome 需要被獨立對待的原因。
Anthropic 是一次具名剖析;拉高到整個產業,同樣模式在更大樣本裡重複出現——一份針對 127 個公開揭露 AI 事件的分析指出,只靠傳統監控能偵測到的 LLM 失敗只佔 32%,剩下 68% 都發生在「技術層完全綠燈、但語義或品質層已在退化」的區間。Why LLM Failures Hide from Traditional Monitoring 其中一例:某系統的 hallucination 品質分數六小時內從 94 分滑落到 82 分,Prometheus 一片綠燈,直到投訴湧入才被發現。
這種漸進式滑落跟第 ⑥ 節情境 A、B 完全不同——A、B 幾分鐘內就能在 RED/USE panel 上看到明顯轉折;語意層的劣化沒有轉折,曲線平滑下降,不會觸發傳統的閾值型告警:技術指標(error rate、latency、utilization)在這六小時內一路平穩、沒有任何轉折,Quality Signal 卻從 94 分平滑下滑到 82 分,找不到單一時間點可觸發傳統閾值告警。
這條曲線對照 32% vs 68% 這個比例,把本節的論點量化:只做到 Golden Signals、RED、USE 三層,理論上限最多只能發現三分之一左右的真實失敗,剩下三分之二只能靠獨立於三套框架之外的 quality evaluation 才有機會被發現——這也是 Day 08 的 evaluator 管線與 Day 23 的 LLM observability 之所以必要的原因。
今天新增了框架和指標,並沒有推翻 Day 21 的分工:Metric 回答「現在有多少 request 慢、失敗、正在排隊」,Log 回答「request_id=abc 的 outcome 為何被映射成 upstream_timeout」,Trace 回答「這個 request 卡在 queue、retrieval、model 還是 tool」。
從 metric panel 點到 trace 的連結,要透過相同的時間範圍、deployment version 和可控的 correlation ID 建立。Prometheus 不適合存每個 request ID;Grafana 的 exemplar、log query 或 trace link 能提供跳轉,但實作方式依 telemetry backend 而異——別把「可點一下」當成整合完成,應自行測一個已知失敗 request 能否走完這條路徑。
放回 Day 21 的三分法:Golden Signals、RED、USE 全屬於「Metric」這一層的組織方式,沒有新增任何訊號類型,只是幫忙決定值班時先看哪幾個——能更快找到哪一段慢了,真正回答「為什麼慢」仍要靠 Log 和 Trace。
具體流程:值班工程師在首頁看到 P95 latency 升高,切到 RED panel 確認是 /ask 在變差,再切到 USE panel 看到 queue depth 同時升高——Metric 已定位到「inference queue 正在累積」,但還沒回答為什麼。這時跳進 Trace 找一條慢請求,確認排隊發生在哪個環節;再跳進 Log 查對應 worker 有沒有錯誤紀錄。三層合起來才構成完整的根因分析。
「從 metric panel 點到 trace 的連結」這句話說起來簡單,實作起來是三層裡最容易半途而廢的一段。從 Grafana 上的 P95 duration panel 出發:先靠 exemplar(Prometheus histogram 附加的樣本點,攜帶「這個樣本對應到哪條 trace」的 trace_id)拿到具體 trace_id;再用同一組 trace_id 切到 Tempo trace explorer,看到這條 trace 的完整 span 樹(retrieval → queue_wait → model → tool);若 span 掛有 request_id 屬性,最後切到 Loki 查結構化 log,取得這次請求的完整細節(outcome 映射依據、例外堆疊、重試次數等 trace 不會記錄的資訊)。
這條路徑有兩個環節最容易斷掉。第一是 exemplar 這一步——它需要 client library 主動支援,且必須在 .observe() 時同步附加 trace context,否則不會產生任何 exemplar,Grafana 顯示不出可點的資料點,卻不會有錯誤訊息告訴你原因。第二是最後跳轉到 log 那一步——若 span 屬性和 log 屬性的 request_id 欄位命名不一致(request.id vs request_id),跳轉在文件上看起來合理,實際點下去卻永遠查不到對應的 log。
這也是為什麼要反覆驗證,而不只是確認 UI 上有「View Trace」按鈕:找一個第 ⑦ 節情境造成的已知失敗請求,實際點一次完整路徑,確認每段跳轉都真能帶到對的地方。
production dashboard 的首頁不需要塞滿所有 metric。比較實際的分層是:首頁放使用者結果、SLO/error budget、P95 TTFT、P95 request duration;服務 drill-down 放 /ask 的 RED、workflow step outcome、deployment 比較;資源 drill-down 放 queue wait/depth、worker、GPU/CPU、connection pool 的 USE;調查頁則可由時間、版本與 correlation 資訊跳到 log 和 trace。
四個分頁之間的導覽關係是單向收斂的:首頁讓任何人在 3 秒內看懂現在好不好(好結果比例、SLO burn、P95 TTFT、P95 duration),點擊後依「哪個服務有問題」分流到服務 drill-down(RED for /ask、workflow steps)或資源 drill-down(queue、GPU、DB pool 的 USE),兩者最終都能收斂到調查頁——依 request_id、deployment 跳轉查詢 log 和 trace。
首頁刻意做少的理由,第 ① 節已用「十幾張圖同時攤開」的反例講過。這裡補一個設計標準:每張首頁圖都該通過「值班工程師剛被叫醒、還沒完全清醒」的測試——需要先理解三個 label 或跟另外兩張圖交叉比對才能看懂的,就不屬於首頁,該挪到 drill-down 頁。首頁的責任只有一個:讓 on-call 的人在幾秒內判斷現在嚴不嚴重、大概哪個方向出問題。
每個 panel 都應有 owner、資料來源、更新頻率、正常範圍的定義,和異常時的第一步——這些是 runbook 的內容,不是 Grafana JSON 裡看不懂的角落。
上線新 prompt 或新 model 前也應先問:新 model route 會怎麼被看見?fallback 有沒有獨立 outcome?queue backlog 會不會先傷到 TTFT?如果答案只能是「上線後再看 dashboard」,代表儀表板還沒準備好。
「每個 panel 都應有 owner、資料來源、更新頻率、正常範圍定義,和異常時的第一步」,最好落地成一份跟 dashboard JSON 分開存放、但緊密對應的規格文件——理由跟第 ⑤ 節的 metric contract 一樣:JSON 是視覺產物容易被隨手改動,panel 的責任定義是治理產物,需要被明確記錄、被 review。一份最小可用的規格:
# observability/panels/user-layer-ttft.yaml
panel: "P95 TTFT"
dashboard: "首頁 — 使用者層"
framework: golden_signals
metric: ai_ttft_seconds
query: |
histogram_quantile(0.95,
sum by (le, model_route) (
rate(ai_ttft_seconds_bucket{route="/ask"}[5m])
)
)
owner: platform-observability
data_source: prometheus
refresh_interval: 30s
normal_range: "< 1.5s"
red_flag_action: >
確認是哪個 model_route 在升高;
primary 升高先查 inference queue depth(USE 資源層 panel);
fallback 升高先查上游 provider 的狀態頁
escalation: "若 P95 TTFT 超過 5s 且持續 10 分鐘,觸發 page,暫停新的 rollout"
last_reviewed: 2026-08-15
last_reviewed 存在的理由:dashboard 的「正常範圍」不是一次定義就永遠有效——隨流量成長、model 換版、基礎設施升級,門檻也需要定期覆核,否則要嘛太寬鬆讓系統早已劣化卻不觸發告警,要嘛太緊造成告警疲勞。red_flag_action 則把「看到紅色時先做什麼」從自由文字升級成跟 dashboard 一起版控、一起 review 的正式欄位——填不出這一欄,正是「這張圖還不該放在首頁」的具體判準。
用 Markdown 或紙筆畫三層,不用選監控產品,也不要用假數字填滿:使用者層放 good outcome ratio、latency SLO、error-budget burn;服務層放 RED for /ask、retrieval、tool;資源層放 worker/GPU queue、connection pool 的 USE。
每張圖附一句「看到紅色時先做什麼」。若答不出來,這張圖還不該放在首頁。
本文未建立任何 dashboard,也沒有連接真實監控資料,三層草圖是規劃練習。
Day22/DIY 專案把這個草圖練習用 Pydantic 模型具體化,變成可被程式檢查的設計文件——app/dashboard.py 定義 DashboardLayer 與 ThreeLayerDashboard,每層強制填入 readers(讀者是誰)與 red_flag_action(看到紅色時的第一步),並有 validate_no_method_confusion() 主動拒絕「User 層或 Service 層被填入 USE 方法欄位」的套錯設計。跑 make generate 看到「All validations passed」,驗的是這份設計文件有沒有違反今天的框架分層原則,不是 dashboard 好不好看。
把 SRE Lab 畫出的三層,對回今天用到的框架,會得到一張對照表:
| 層級 | 框架 | 例子指標 | 看到紅色先做什麼 |
|---|---|---|---|
| 使用者層 | Golden Signals | good outcome ratio、latency SLO burn | 先看 error budget 燒多快,決定要不要暫停 rollout |
| 服務層 | RED | /ask、retrieval、tool 各自的 rate/errors/duration |
對照 trace,找出是哪一段拖慢整體 duration |
| 資源層 | USE | GPU utilization、inference queue 長度 | 確認是不是資源撐不住,而不是程式邏輯出錯 |
這張表沒有接上任何監控後端,只是 SRE Lab 產出的規劃文件,無法回放觀測資料——這點刻意和前幾天「有 fixture、有假資料可查」的 Evidence 不同,因為今天的任務是選框架,不是量測。前幾天驗證「某個元件行為對不對」天然適合固定資料回放;今天驗證的是「選框架的邏輯對不對」,更接近架構圖畫得合不合理。用 Pydantic 模型把三層邏輯寫成可驗證的程式碼(第 ⑪ 節),已是這個層級最接近「可回放測試」的形式——它驗證規則有沒有被違反,不是資料。
Netflix 的 Latency Monkey 實驗給了一個具體例子:在生產環境主動注入延遲後,工程師才發現許多服務缺乏 timeout 設定。只看 RED 裡的 Rate(流量沒變)與 Errors(沒報錯)完全看不出異常;要看 Duration,尤其 P99,才會發現系統在人為延遲下會無限等待。
USE 方法的價值也不只是理論。Brendan Gregg 在 Netflix 期間直接以 USE 為基礎開發了 Vector——一個即時的 instance-level 效能分析工具,快速定位 CPU、磁碟、網路是不是瓶頸。Introducing Vector: Netflix's On-Host Performance Monitoring Tool 它把 USE 從理論性的診斷 checklist,變成故障現場幾秒鐘就能看到「哪類資源在飽和」的操作介面。
這兩個案例是同一教訓的兩個互補面向:Latency Monkey 說明你需要主動製造異常,才會發現框架裡哪個維度沒被好好利用;Vector 說明框架選對維度後還需要工具把它變成真正好用的東西。光把三套框架寫進團隊 wiki 不夠,還要像 Latency Monkey 一樣用受控實驗(第 ⑦ 節的情境 A、B)驗證每個維度真的被正確實作,也要像 Vector 一樣把框架落地成真正好操作的工具。
回頭看今天引用過的案例——Borg 的 scheduling delay、OpenAI 的 saturation 先於 utilization 亮起、Slack 的 utilization 方向性失真、Netflix 的 rate/errors 正常但 duration 出事——分屬不同公司、不同年代,卻共享同一個結構:utilization 這個最直觀的指標,分別以「不夠」「被掩蓋」「方向錯誤」「缺席」四種方式讓人做出錯誤判斷。USE 把 Utilization、Saturation、Errors 拆成三個獨立問題,正是為了不讓任何單一指標扛起它結構上扛不起的責任——queue depth 不能單獨扛起「排隊嚴不嚴重」,HTTP 成功率不能單獨扛起「使用者滿不滿意」。
如果這是要上線的 production system,我會做三件事:把 dashboard 明確拆成使用者層、服務層、資源層三張,禁止同一張圖混放三種層級的數字;替每張圖寫一句「看到異常時的第一個動作」,讓草圖變成可執行的 runbook;把 AI workflow 特有的訊號(queue delay、TTFT、tool failure、evaluation outcome)明確掛回使用者層。
另外還會補兩個容易被忽略但代價很高的項目。一是為 outcome 映射寫單元測試,把第 ⑧ 節「outcome 映射不一致」直接變成 CI 裡會失敗的案例,而非留到值班時才靠人工發現。二是為每個 Gauge 類指標明確定義「異常終止時誰負責歸零」——應用層的 finally、sidecar 重置、還是 queue 系統自己的權威來源;講不清楚,ai_inference_queue_depth 遲早會在某次 worker crash 後留下永遠不會恢復的偏移量,讓值班工程師對著假警報浪費判斷時間。
選對框架,決定 on-call 是花五分鐘還是五十分鐘找到問題;框架本身不會替你決定產品該承諾什麼。今天先把 Golden Signals、RED、USE 三層分清楚,明天把 LLM trace 補完整,讓每個 workflow 節點都有可追的路徑。
下一篇會回到 Day 01 的第一條 trace,重新理解「LLM Trace 其實就是 AI Workflow 的 distributed trace」這件事。
這篇是 Learning SRE for the AI Era 系列的一部分。
我會從 SRE 的服務可靠性基礎開始,逐步探索當系統加入 LLM、RAG、Agent 與 GPU Infrastructure 後,如何讓 AI 系統不只可用,也能被觀測、評估、控制成本並安全演進。
Build → Trace → Break → Measure → Evaluate → Recover → Improve.